package edu.moravian.math;

import java.util.Random;


/**
 * This class represents a two-dimensional vector. For efficiency, the class offers two versions
 * of most operations: one mutates the vector (this avoids the need to create a new object) 
 * and the other computes the result as a new vector without changing any components of the 
 * computation. In addition, because computations of the form v + d * u are common 
 * (u, v are vectors; d is a scalar), they are supported as a single method. 
 * The static class value TOL is used in *all* computations to check for value 
 * close to zero. (see note on this field) 
 * 
 * @author Elliot
 */
public class Vector2D 
{

    /**
     * Represents a true zero vector 
     */
    public static final Vector2D zero = new Vector2D();
    
    /**
     * Represents a north heading vector
     */
    public static final Vector2D north = new Vector2D(0, 1);
    
    /**
     * Represents an east heading vector
     */
    public static final Vector2D east = new Vector2D(1, 0);

    /**
     * Represents a south heading vector
     */
    public static final Vector2D south = new Vector2D(0, -1);
    
    /**
     * Represents a west heading vector
     */
    public static final Vector2D west = new Vector2D(-1, 0);
    

    /**
     * TOL is the threshold for determining whether a component "goes to zero." 
     * In any vector where |x| < TOL or |y| < TOL, then that value becomes zero. 
     */
    public static final double TOL = 10e-8;
    
    private double x;
    private double y;
 
    /**
     * Create a zero vector 
     */
    public Vector2D()
    {
        this.x = 0.0;
        this.y = 0.0;
    }
    
    /**
     * Create a vector equal to the specified one
     * 
     * @param x the x component of the new vector
     * @param y the y component of the new vector 
     */
    public Vector2D(double x, double y)
    {
        this.x = x;
        this.y = y;
        
        checkForZeros();
    }
    
    /**
     * Create a vector equal to the specified one 
     * 
     * @param v the vector to "copy"
     */
    public Vector2D(Vector2D v)
    {
        this.x = v.x;
        this.y = v.y;
        
        checkForZeros();
    }
    
    /**
     * Get the x value of the vector 
     * 
     * @return the x component
     */
    public double getX()
    {
        return x;
    }
    
    
    /**
     * Get the y value of the vector 
     * 
     * @return the y component
     */
    public double getY()
    {
        return y;
    }
    
    /**
     * Change the value of the vector to the specified values. 
     * 
     * @param x the new x value
     * @param y the new y value 
     */
    public void set(double x, double y)
    {
        this.x = x;
        this.y = y;
        
        checkForZeros();
    }
    
    
    /**
     * Change the value of the vector to equal the parameter 
     * 
     * @param v the vector to "copy"
     */
    public void set(Vector2D v)
    {
        x = v.x;
        y = v.y;
        
        checkForZeros();
    }
    
    /**
     * Get the magnitude of the vector 
     * 
     * @return the magnitude
     */
    public double magnitude()
    {
        return Math.sqrt(x*x + y*y);
    }
    
    
    /**
     * Get the square of the magnitude 
     * 
     * @return the square of the magnitude
     */
    public double magnitudeSq()
    {
        return x*x + y*y;
    }
    
    
    /**
     * Determine the angle of the vector as measured from the positive x axis. 
     * By definition, the angle of a 0 vector is zero. 
     * 
     * @return an angle in radians from -PI to PI
     */
    public double angle()
    {
        if(this.equals(zero))
        {
            return 0.0;
        }
        
        return Math.atan2(y, x);
    }
    
    
    /**
     * Compute the right orthogonal vector without changing this vector 
     * 
     * @return the right orthogonal vector
     */
    public Vector2D getRightOrtho()
    {
        return new Vector2D(y, (-1*x));
    }
    
    
    /**
     * Change this vector to equal its right orthogonal vector. 
     */
    public void setRightOrtho()
    {
        double temp = x;
        x = y;
        y = -1*temp;
    }
    
    
    /**
     * Compute the left orthogonal vector without changing this vector 
     * 
     * @return the left orthogonal vector
     */
    public Vector2D getLeftOrtho()
    {
        return new Vector2D((-1*y), x);
    }
    
    /**
     * Change this vector to equal its left orthogonal vector. 
     */
    public void setLeftOrtho()
    {
        double temp = x;
        x = -1*y;
        y = temp;
    }
    
    /**
     * Change this vector to one in the same direction with magnitude of 1. 
     * If the vector is the zero vector, it is unchanged. 
     */
    public void normalize()
    {
        double mag = magnitude();
        
        if(this.equals(zero) == false)
        {
            x /= mag;
            y /= mag;
            
            checkForZeros();
        }
    }
    
    
    /**
     * Compute the normalized vector (same direction with magnitude 1) without changing
     * this vector. If this vector is the zero vector, a zero vector will be returned (a new object). 
     * 
     * @return 
     */
    public Vector2D getNormalized()
    {
        double mag = magnitude();
        
        if(this.equals(zero) == false)
        {
            double newX = x/mag;
            double newY = y/mag;
            
            return new Vector2D(newX, newY);
        }
        else
        {
            return new Vector2D();
        }
    }
    
    
    /**
     * Compute the sum of this vector and the parameter without changing this vector: return this + sum 
     * 
     * @param rhs the right hand side of the addition 
     * 
     * @return the sum of the two vectors
     */
    public Vector2D plus(Vector2D rhs)
    {
        double newX = x+rhs.x;
        double newY = y+rhs.y;
        
        return new Vector2D(newX, newY);
    }
    
    
    /**
     * Make this vector equal to the sum of itself and the specified vector: this = this + rhs 
     * 
     * @param rhs the vector to add (right hand side of the sum) 
     */
    public void plusEquals(Vector2D rhs)
    {
        x += rhs.x;
        y += rhs.y;
        
        checkForZeros();
    }
    
    /**
     * Compute the difference of this vector and the parameter without changing this vector: return this - rhs 
     * 
     * @param rhs the right hand side of the difference 
     *
     * @return the difference of the two vectors
     */
    public Vector2D minus(Vector2D rhs)
    {
        double newX = x-rhs.x;
        double newY = y-rhs.y;
        
        return new Vector2D(newX, newY);
    }
    
    
    /**
     * Make this vector equal to the difference of itself and the specified vector: this = this - rhs 
     * 
     * @param rhs the vector to subtract (right hand side of the difference) 
     */
    public void minusEquals(Vector2D rhs)
    {
        x -= rhs.x;
        y -= rhs.y;
        
        checkForZeros();
    }
    
    /**
     * Compute the product of this vector and a scalar without changing this vector: return d * this 
     * 
     * @param d the scalar 
     * 
     * @return the product of the scalar and the vector
     */
    public Vector2D times(double d)
    {
        double newX = x*d;
        double newY = y*d;
        
        return new Vector2D(newX, newY);
    }
    
    /**
     * Make this vector equal to the product of itself and the scalar: this = d * this 
     * 
     * @param d the scalar
     */
    public void timesEquals(double d)
    {
        x *= d;
        y *= d;
        
        checkForZeros();
    }
    
    
   
    /**
     * Compute the product of this vector and the inverse of a scalar without changing this vector: return 1/d * this 

     * @param d the scalar 
     * 
     * @return the product of the inverse of the scalar and the vector 
     * 
     * @throws IllegalArgumentException - if d is 0 (using == comparison)
     */
    public Vector2D divide(double d) throws IllegalArgumentException
    {
        if(d == 0.0)
        {
            throw new IllegalArgumentException("Cant divide a vector by zero");
        }
        
        double newX = x/d;
        double newY = y/d;
        
        return new Vector2D(newX, newY);   
    }
   
    
    /**
     * Make this vector equal to the product of itself and the inverse of the scalar: this = 1/d * this 
     * 
     * @param d the scalar 
     * 
     * @throws IllegalArgumentException - if d is 0 (using == comparison)
     */
    public void divideEquals(double d) throws IllegalArgumentException
    {
        if(d == 0.0)
        {
            throw new IllegalArgumentException("Cant divide a vector by zero");
        }
        
        x /= d;
        y /= d;
        
        checkForZeros();
    }

    
    /**
     * Change this vector to the negation of itself: this = -1 * this 
     */
    public void negate()
    {
        x *= -1;
        y *= -1;
    }
    
    
    /**
     * Change the sign on the x component. If v = (x, y), then this method will make it v = (-x, y) 
     */
    public void reflectX( )
    {
        x *= -1;
    }
    
    
    /**
     * Change the sign on the y component. If v = (x, y), then this method will make it v = (x, -y) 
     */
    public void reflectY()
    {
        y *= -1;
    }
    
    /**
     * Compute the dot product of this vector and the parameter: return this * rhs (where '*' is the dot product) 
     * 
     * @param rhs the right hand side of the dot product 
     * 
     * @return the dot product of the two vectors
     */
    public double dot(Vector2D rhs)
    {
        return x*rhs.x + y*rhs.y;
    }
            
    /**
     * Compute the sum of this vector and v scaled by "scalar": return this + scalar * v - This computation will not change this vector 
     * 
     * @param scalar the scalar
     * @param v the vector 
     * 
     * @return the Vector result from the sum
     */
    public Vector2D scalePlus(double scalar, Vector2D v)
    {
        double newX = x + (v.x * scalar);
        double newY = y + (v.y * scalar);
        
        return new Vector2D(newX, newY);
    }
    
    
    /**
     * Change this vector to the sum of this vector and the scaled vector v: this = this + scalar * v 
     * 
     * @param scalar the scalar
     * @param v the vector
     */
    public void scalePlusEquals(double scalar, Vector2D v)
    {
        x += (v.x * scalar);
        y += (v.y * scalar);
        
        checkForZeros();
    }
    
    /**
     * If the magnitude of the vector is less than max, then the vector is unchanged. Otherwise, the vector is truncated so that it 
     * points in the same direction but has a magnitude of max
     * 
     * @param max the maximum allowed magnitude 
     */
    public void truncate(double max)
    {
        double mag = magnitude();
        
        if(mag > max)
        {
            double ratio = max/mag;
            
            x *= ratio;
            y *= ratio;
            
            checkForZeros();
        }
    }
    
    /**
     * Two vectors are equal if both the x and y components are within TOL of each other 
     * 
     * NOTE: This violates object's contract for equals - Violates transitive 
     * 
     * @param aThat the object to compare 
     * 
     * @return true of they are the same, false otherwise
     */
    @Override
    public boolean equals(Object aThat)
    {
        if(this == aThat)
        {
            return true;
        }
        
        if(aThat instanceof Vector2D == false)
        {
            return false;
        }
        
        Vector2D that = (Vector2D)aThat;
        
        double xDiff = Math.abs(this.x - that.x);
        double yDiff = Math.abs(this.y - that.y);
        
        if(xDiff < TOL && yDiff < TOL)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    
    /**
     * Compute the hash value for this vector 
     * 
     * NOTE: since equals violates its contract this will also since it uses Double.toLongBits 
     * instead of the value for the hash
     * 
     * @return the has value
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 41 * hash + (int) (Double.doubleToLongBits(this.x) ^ (Double.doubleToLongBits(this.x) >>> 32));
        hash = 41 * hash + (int) (Double.doubleToLongBits(this.y) ^ (Double.doubleToLongBits(this.y) >>> 32));
        return hash;
    }
    
    
    /**
     * A string of the form "(x, y)" 
     * 
     * @return the string representing the vector
     */
    @Override
    public String toString()
    {
        return "(" + x + ", " + y + ")";
    }
    
    /**
     * public static Vector2D randomVectorFixedMagnitude(double magnitude)
     * 
     * @param magnitude the magnitude of the vector returned 
     * 
     * @return a random vector with the given magnitude
     */
    public static Vector2D randomVectorFixedMagnitude(double magnitude)
    {
        Random random = new Random();
        
        //create a quadrant one random vector with a fixed mag
        double magSq = magnitude * magnitude;
        
        double newX = random.nextDouble() * magSq;
        double newY = magSq - newX;
        
        newX = Math.sqrt(newX);
        newY = Math.sqrt(newY);
        
        //randomize the quadrant
        if(random.nextBoolean())
        {
            newX *= -1;
        }
        if(random.nextBoolean())
        {
            newY *= -1;
        }
  
        return new Vector2D(newX, newY);
    }
    
    /**
     * Generate a random vector with a magnitude no more than the given value 
     * 
     * @param magnitude the maximum magnitude 
     * 
     * @return a random vector 
     */
    public static Vector2D randomVectorMaxMagnitude(double maxMagnitude)
    {
        Random random = new Random();

        //create a quadrant one random vector with a random mag
        double magSq = maxMagnitude * maxMagnitude;
        
        double newX = random.nextDouble() * magSq;
        double newY = random.nextDouble() * (magSq - newX);
        
        newX = Math.sqrt(newX);
        newY = Math.sqrt(newY);
        
        //randomize the quadrant
        if(random.nextBoolean())
        {
            newX *= -1;
        }
        if(random.nextBoolean())
        {
            newY *= -1;
        }
        
        return new Vector2D(newX, newY);
    }
    
    
    private void checkForZeros()
    {
        if(Math.abs(x) < TOL)
        {
            x = 0.0;
        }
        if(Math.abs(y) < TOL)
        {
            y = 0.0;
        }
    }
}
